﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace HenIT.Text
{    
    /// <summary>
    /// HTML writer base class intended for generic functionality
    /// </summary>
    public abstract class HTMLWriterBase : IDisposable
    {
        #region Local vars
        /// <summary>
        /// Underlaying stream used for writing html to
        /// </summary>
        protected StringBuilder sbHtmlContent = new StringBuilder();
        /// <summary>
        /// Underlaying stream used for stylesheet content
        /// </summary>
        protected StringBuilder sbStyleContent = new StringBuilder();
        /// <summary>
        /// List of external stylesheets
        /// </summary>
        protected List<string> externalStyleSheets = new List<string>();
        /// <summary>
        /// Stack of html tags added
        /// </summary>
        protected Stack<string> tags = new Stack<string>();
        protected bool lastWrittenIndentation = false;
        protected bool lastWrittenCRLF = true;

        /// <summary>
        /// html start tags that must start on new line. Only applies on autoformatting.
        /// </summary>
        protected string[] startTagsAutoOnNewLine = new string[] { };
        /// <summary>
        /// html tags that cause the indentation level to increase or decrease. Only applies on autoformatting.
        /// </summary>
        protected string[] tagsAutoIncDecIndentation = new string[] { };
        /// <summary>
        /// html end tags that must start on new line. Only applies on autoformatting.
        /// </summary>
        protected string[] endTagsAutoOnNewLine = new string[] { };
        /// <summary>
        /// html end tags where a CRLF character set is appended. Only applies on autoformatting.
        /// </summary>
        protected string[] endTagsAutoAppendCRLF = new string[] { };

        protected static string specialCharacterArray;
        protected static string specialLetterCharacterArray;
        protected static string specialSymbolCharacterArray;
        #endregion

        #region Properties
        /// <summary>
        /// Html document title
        /// </summary>
        public string DocumentTitle { get; set; }
        /// <summary>
        /// Indentation level
        /// </summary>
        public int Indentation { get; set; }
        /// <summary>
        /// Autoformatting enabled
        /// </summary>
        public bool AutoFormatting { get; set; }
        /// <summary>
        /// Autoindentation enabled
        /// </summary>
        public bool AutoIndentation { get; set; }
        private int indentationCharCount = 2;
        /// <summary>
        /// Number of characters used for indentation
        /// </summary>
        public int IndentationCharCount
        {
            get { return indentationCharCount; }
            set { indentationCharCount = value; }
        }
        private char indentationChar = ' ';
        /// <summary>
        /// Character used for indentation
        /// </summary>
        public char IndentationChar
        {
            get { return indentationChar; }
            set { indentationChar = value; }
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Created default instance
        /// </summary>
        public HTMLWriterBase()
        {
            AutoFormatting = false;
            AutoIndentation = false;
            Indentation = 0;

            InitializingSpecialCharacters();
            InitializeTagFormatArrays();
        }
        /// <summary>
        /// Creates instance with html document title specified
        /// </summary>
        /// <param name="documentTitle">Html document title</param>
        public HTMLWriterBase(string documentTitle) : this()
        {
            DocumentTitle = documentTitle;
        }
        /// <summary>
        /// Stub for initialization of the tag auto formatting arrays.
        /// </summary>
        protected abstract void InitializeTagFormatArrays();
        private static void InitializingSpecialCharacters()
        {
            specialCharacterArray = "";
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < Enum.GetNames(typeof(HTMLSpecialCharacter)).Length; i++)
            {
                sb.Append(StringValueAttributeUtil.GetStringValue((HTMLSpecialCharacter)i));
            }
            specialCharacterArray = sb.ToString();
            specialLetterCharacterArray = "";
            sb = new StringBuilder();
            for (int i = 0; i < Enum.GetNames(typeof(HTMLSpecialLetterCharacter)).Length; i++)
            {
                sb.Append(StringValueAttributeUtil.GetStringValue((HTMLSpecialLetterCharacter)i));
            }
            specialLetterCharacterArray = sb.ToString();
            specialSymbolCharacterArray = "";
            sb = new StringBuilder();
            for (int i = 0; i < Enum.GetNames(typeof(HTMLSpecialSymbolCharacter)).Length; i++)
            {
                sb.Append(StringValueAttributeUtil.GetStringValue((HTMLSpecialSymbolCharacter)i));
            }
            specialSymbolCharacterArray = sb.ToString();
        }
        #endregion

        #region Destructor
        ~HTMLWriterBase()
        {            
            this.Dispose(false);
        }
        #endregion

        #region IDisposable Members
        protected virtual void Dispose(bool disposing)
        {
            //sbHtmlContent.Clear();
            if (disposing)
            {
                GC.SuppressFinalize(this);
            }
        }
        public void Dispose()
        {
            this.Dispose(true);
        }
        #endregion

        #region AppendInternal
        /// <summary>
        /// Appends text to the underlaying stream
        /// </summary>
        /// <param name="value">Text to append</param>
        protected void AppendInternal(string value)
        {
            if (!string.IsNullOrEmpty(value))
            {
                sbHtmlContent.Append(value);
                lastWrittenIndentation = false;
                lastWrittenCRLF = false;
            }
        }
        /// <summary>
        /// Appends a CRLF set of characters to the stream
        /// </summary>
        protected void AppendInternalCRLF()
        {
            sbHtmlContent.Append("\r\n");
            lastWrittenIndentation = false;
            lastWrittenCRLF = true;
        }
        /// <summary>
        /// Appends a CRLF plus current indentation level spacing to stream
        /// </summary>
        protected void AppendInternalIndentation()
        {
            sbHtmlContent.Append(new string(indentationChar, indentationCharCount * Indentation));
            lastWrittenIndentation = true;
        }
        #endregion

        #region Indentation and new line control
        /// <summary>
        /// Appends CRLF set of characters only if autoformatting and no previous CRLF was added so far to the stream
        /// </summary>
        protected void AppendNewLineInternal()
        {
            if (AutoFormatting && !lastWrittenCRLF)
            {
                AppendInternalCRLF();
            }
        }
        /// <summary>
        /// Appends CRLF set of characters and indentation only if autoformatting and no previous indentation was added to the stream
        /// </summary>
        protected void AppendNewLineWithIndentationInternal()
        {
            if (AutoFormatting && !lastWrittenIndentation)
            {
                AppendInternalCRLF();
                AppendIndentation();
            }
        }
        /// <summary>
        /// Increments the indentation level
        /// </summary>
        protected void IndentationInc()
        {
            Indentation++;
        }
        /// <summary>
        /// Decrements the indentation level
        /// </summary>
        protected void IndentationDec()
        {
            if (Indentation > 0)
                Indentation--;
        }
        /// <summary>
        /// Appends indentation characters to stream
        /// </summary>
        protected void AppendIndentation()
        {
            AppendInternalIndentation();
        }
        #endregion

        #region Tag handling
        /// <summary>
        /// Appends start tag to stream
        /// </summary>
        /// <param name="tagName">Name of start tag to append</param>
        /// <param name="className">CSS class name</param>
        /// <param name="customAttributes">Custom attributes</param>
        protected void AppendTagStartInternal(string tagName, string className, params CustomAttribute[] customAttributes)
        {
            if (AutoFormatting)
            {
                if (startTagsAutoOnNewLine.Contains(tagName.ToLower()))
                {
                    AppendNewLineInternal();
                    AppendIndentation();
                }
                else if (lastWrittenCRLF)
                {
                    AppendIndentation();
                }
            }

            this.AppendInternal(string.Format("<{0}", tagName));
            if (className.Length > 0)
                this.AppendInternal(string.Format(" class=\"{0}\"", className));
            foreach (CustomAttribute customAttribute in customAttributes)
            {
                this.AppendInternal(" " + customAttribute.ToString());
            }
            this.AppendInternal(">");
            tags.Push(tagName);
            if (AutoFormatting)
            {
                if (tagsAutoIncDecIndentation.Contains(tagName.ToLower()))
                {
                    IndentationInc();
                    AppendNewLineInternal();
                }
            }
        }
        /// <summary>
        /// Appends end tag to stream
        /// </summary>
        /// <param name="tagName">Name of end tag to append</param>
        /// <param name="autoPop">Automatically pop tag from stack</param>
        protected void AppendTagEndInternal(string tagName, bool autoPop)
        {
            if (autoPop && tags.Peek() == tagName)
                tags.Pop();

            if (AutoFormatting)
            {
                if (tagsAutoIncDecIndentation.Contains(tagName.ToLower()))
                {
                    IndentationDec();
                }
                if (endTagsAutoOnNewLine.Contains(tagName.ToLower()))
                {
                    AppendNewLineInternal(); ;
                    AppendIndentation();
                }
                else if (lastWrittenCRLF)
                {
                    AppendIndentation();
                }
            }

            this.AppendInternal("</" + tagName + ">");

            if (AutoFormatting)
            {
                if (endTagsAutoAppendCRLF.Contains(tagName.ToLower()))
                {
                    AppendNewLineInternal();
                }
            }
        }
        #endregion

        #region Stylesheets
        #region Internal Stylesheet
        /// <summary>
        /// Appends stylesheet information to stylesheet stream
        /// </summary>
        /// <param name="styleName">Name of tag and/or class and/or id for CSS</param>
        /// <param name="styleContent">Style description</param>
        protected void AppendStyleInternal(string styleName, string styleContent)
        {
            sbStyleContent.AppendLine(styleName + "\r\n{" + styleContent + "}");
        }
        #endregion

        #region External stylesheets
        /// <summary>
        /// Appends external stylesheet to list of external stylesheets
        /// </summary>
        /// <param name="url">URL of CSS file</param>
        protected void AppendExternalStylesheetInternal(string url)
        {
            externalStyleSheets.Add(url);
        }
        #endregion
        #endregion

        #region Outputs
        /// <summary>
        /// Outputs only the html fragment created in the stream
        /// </summary>
        /// <returns></returns>
        public string ToHtmlFragment()
        {
            return sbHtmlContent.ToString();
        }
        /// <summary>
        /// Outputs the complete html document with html and stylesheets
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            StringBuilder sbFullHtml = new StringBuilder();
            sbFullHtml.AppendLine("<html>")
                .AppendLine("<head>")
                .AppendLine("<title>" + EscapeText(DocumentTitle) + "</title>");
            foreach (string s in externalStyleSheets)
            {
                sbFullHtml.AppendLine(string.Format("<link rel=\"stylesheet\" type=\"text/css\" href=\"{0}\" />", s));
            }
            if (sbStyleContent.Length > 0)
            {
                sbFullHtml.AppendLine("<style type=\"text/css\">")
                    .AppendLine(sbStyleContent.ToString())
                    .AppendLine("</style>");
            }
            sbFullHtml.AppendLine("</head>")
                .AppendLine("<body>")
                .AppendLine(ToHtmlFragment())
                .AppendLine("</body>")
                .AppendLine("</html>");
            return sbFullHtml.ToString();
        }
        /// <summary>
        /// Write html document to a file
        /// </summary>
        /// <param name="outputFileName">Output file path</param>
        public void WriteToFile(string outputFileName)
        {
            System.IO.File.WriteAllText(outputFileName, ToString(), Encoding.UTF8);
        }
        #endregion

        #region static utility methods
        public static string GetEscapedSpecialCharacter(HTMLSpecialCharacter htmlSpecialCharacter)
        {
            return StringValueAttributeUtil.GetStringExpandedValue(htmlSpecialCharacter);
        }
        public static string GetEscapedSpecialCharacter(HTMLSpecialLetterCharacter htmlSpecialCharacter)
        {
            return StringValueAttributeUtil.GetStringExpandedValue(htmlSpecialCharacter);
        }
        public static string GetEscapedSpecialCharacter(HTMLSpecialSymbolCharacter htmlSpecialCharacter)
        {
            return StringValueAttributeUtil.GetStringExpandedValue(htmlSpecialCharacter);
        }
        public static bool ContainsAnySpecialCharacters(string text)
        {
            return text.IndexOfAny(specialCharacterArray.ToCharArray()) > -1 ||
                text.IndexOfAny(specialLetterCharacterArray.ToCharArray()) > -1 ||
                text.IndexOfAny(specialSymbolCharacterArray.ToCharArray()) > -1;
        }
        public static string EscapeText(string text, bool ignoreAmp = false)
        {
            if (string.IsNullOrEmpty(text))
                return "";
            else
            {
                if (string.IsNullOrEmpty(specialCharacterArray))
                {
                    InitializingSpecialCharacters();
                }
                bool containsSpecialChars = text.IndexOfAny(specialCharacterArray.ToCharArray()) > -1;
                bool containsSpecialLetterChars = text.IndexOfAny(specialLetterCharacterArray.ToCharArray()) > -1;
                bool containsSpecialSymbolChars = text.IndexOfAny(specialSymbolCharacterArray.ToCharArray()) > -1;
                string escapedText = text;
                if (containsSpecialChars && specialCharacterArray.Length > 0)
                    for (int i = 0; i < specialCharacterArray.Length; i++)
                        if (!ignoreAmp || specialCharacterArray.Substring(i, 1) != "&")
                            escapedText = escapedText.Replace(specialCharacterArray.Substring(i,1), StringValueAttributeUtil.GetStringExpandedValue((HTMLSpecialCharacter)i));
                if (containsSpecialLetterChars && specialLetterCharacterArray.Length > 0)
                    for (int i = 0; i < specialLetterCharacterArray.Length; i++)
                        escapedText = escapedText.Replace(specialLetterCharacterArray.Substring(i, 1), StringValueAttributeUtil.GetStringExpandedValue((HTMLSpecialLetterCharacter)i));
                if (containsSpecialSymbolChars && specialSymbolCharacterArray.Length > 0)
                    for (int i = 0; i < specialSymbolCharacterArray.Length; i++)
                        escapedText = escapedText.Replace(specialSymbolCharacterArray.Substring(i, 1), StringValueAttributeUtil.GetStringExpandedValue((HTMLSpecialSymbolCharacter)i));
                return escapedText;
            }
        }
        #endregion
    }
}
